/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkFile } from "arkanalyzer/lib";
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import Logger, { LOG_MODULE_TYPE } from "arkanalyzer/lib/utils/logger";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { Defects, IssueReport } from "../../model/Defects";
import { Rule } from "../../model/Rule";
import { FileMatcher, MatcherCallback, MatcherTypes } from "../../Index";
import { RuleFix } from "../../model/Fix";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, "CommaSpacingCheck");

const gMetaData: BaseMetaData = {
  severity: 2,
  ruleDocPath: "docs/comma-spacing-check.md",
  description: "Enforce consistent spacing before and after commas",
};

interface CommaSpacingOption {
  before?: boolean;
  after?: boolean;
}

interface Range {
  start: number;
  end: number;
}

export class CommaSpacingCheck implements BaseChecker {
  readonly metaData: BaseMetaData = gMetaData;
  public rule: Rule;
  public defects: Defects[] = [];
  public issues: IssueReport[] = [];
  private arkFile: ArkFile;

  private options: CommaSpacingOption = {
    before: false,
    after: true,
  };

  private inMultiLineComment: boolean = false;

  public check(target: any): void {
    this.options = this.rule.option[0] ? this.rule.option[0] : this.options;
    if (target instanceof ArkFile) {
      this.arkFile = target;
      const code = target.getCode();
      if (!code) {
        return;
      }

      const lines = code.split("\n");
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        this.checkLine(line, i + 1, target.getFilePath());
      }
    }
  }

  public registerMatchers(): MatcherCallback[] {
    const matcher: FileMatcher = {
      matcherType: MatcherTypes.FILE,
    };

    const matchCallback: MatcherCallback = {
      matcher: matcher,
      callback: this.check.bind(this),
    };

    return [matchCallback];
  }

  private checkLine(line: string, lineNumber: number, filePath: string): void {
    if (line.trim().startsWith("//") || line.trim().startsWith("/*")) {
      if (line.trim().startsWith("/*") && !line.includes("*/")) {
        this.inMultiLineComment = true;
      }
      return;
    }

    if (this.inMultiLineComment) {
      if (line.includes("*/")) {
        this.inMultiLineComment = false;
      }
      return;
    }

    const { stringRanges, regExpRanges, commentRanges } = this.cacheRanges(line);
    
    let effectiveLine = line;
    let skipRanges: [number, number][] = commentRanges.map(range => [range.start, range.end]);

    const commaRegex = /,/g;
    let match;
    while ((match = commaRegex.exec(effectiveLine)) !== null) {
      const commaIndex = match.index;

      if (this.isInRanges(commaIndex, stringRanges)) {
        continue;
      }

      if (this.isInRanges(commaIndex, regExpRanges)) {
        continue;
      }

      const isGenericTrailingComma = this.isGenericTrailingComma(
        effectiveLine,
        commaIndex
      );
      if (isGenericTrailingComma) {
        continue;
      }

      const isInCommentContent = skipRanges.some(
        ([start, end]) => commaIndex > start && commaIndex < end
      );
      if (isInCommentContent) {
        continue;
      }

      let prevChar = effectiveLine[commaIndex - 1];
      let nextChar = effectiveLine[commaIndex + 1];

      const isBeforeComment = skipRanges.some(
        ([start]) => start === commaIndex + 1
      );
      const isImmediatelyAfterComment = skipRanges.some(
        ([_, end]) => end === commaIndex - 1
      );
      const isCommaBeforeComment = this.isCommaBeforeComment(
        effectiveLine,
        commaIndex,
        skipRanges
      );

      if (isBeforeComment) {
        nextChar = " ";
      }
      if (isImmediatelyAfterComment) {
        prevChar = " ";
      }

      const nextCommaIndex = effectiveLine.indexOf(",", commaIndex + 1);
      const prevCommaIndex = effectiveLine.lastIndexOf(",", commaIndex - 1);

      const isInArray =
        effectiveLine.includes("[") && effectiveLine.includes("]");

      const isEmptyArrayElement =
        isInArray &&
        ((effectiveLine.slice(0, commaIndex).trim().endsWith("[") &&
          !effectiveLine
            .slice(commaIndex + 1)
            .trim()
            .startsWith("]")) ||
          (nextCommaIndex !== -1 &&
            effectiveLine.substring(commaIndex + 1, nextCommaIndex).trim() ===
              "") ||
          (prevCommaIndex !== -1 &&
            effectiveLine.substring(prevCommaIndex + 1, commaIndex).trim() ===
              ""));

      const isTrailingComma =
        isInArray &&
        effectiveLine
          .slice(commaIndex + 1)
          .trim()
          .replace(/\s*\/\*[\s\S]*?\*\/\s*/g, "")
          .replace(/\s*\/\/.*/g, "")
          .startsWith("]");

      const isConsecutiveComma =
        isEmptyArrayElement ||
        (nextCommaIndex !== -1 &&
          effectiveLine.substring(commaIndex + 1, nextCommaIndex).trim() ===
            "") ||
        (prevCommaIndex !== -1 &&
          effectiveLine.substring(prevCommaIndex + 1, commaIndex).trim() ===
            "");

      const isLineStart = effectiveLine.substring(0, commaIndex).trim() === "";

      const isAfterLeftBracket =
        prevChar === "[" ||
        (prevChar === " " &&
          effectiveLine.slice(0, commaIndex).trim().endsWith("["));
      const isBeforeRightBracket =
        (nextChar === "]" ||
          (nextChar === " " &&
            effectiveLine
              .slice(commaIndex + 1)
              .trim()
              .startsWith("]"))) &&
        !isTrailingComma;
      const isBeforeRightBrace =
        nextChar === "}" ||
        (nextChar === " " &&
          effectiveLine
            .slice(commaIndex + 1)
            .trim()
            .startsWith("}"));
      const isBeforeRightParen =
        nextChar === ")" ||
        (nextChar === " " &&
          effectiveLine
            .slice(commaIndex + 1)
            .trim()
            .startsWith(")"));

      const hasSpaceBefore = prevChar === " ";
      const hasSpaceAfter = nextChar === " ";
      const isLineEnd = commaIndex === effectiveLine.trim().length - 1;
      const isAfterComment = this.isCommaAfterComment(
        effectiveLine,
        commaIndex
      );
      const skipTrailingCommaAfterCheck = this.options.before === false && this.options.after === true && isTrailingComma;
      const skipAfterSpaceCheck = !this.options.after && isCommaBeforeComment;
      const isEndOfLine = this.isEndOfLine(effectiveLine, commaIndex);

      if (
        isConsecutiveComma ||
        isAfterLeftBracket ||
        (isBeforeRightBracket && !isTrailingComma) ||
        isBeforeRightBrace ||
        isBeforeRightParen ||
        isEmptyArrayElement
      ) {
        continue;
      }

      const actualColumn = commaIndex;
      const absolutePos = this.calculateAbsolutePosition(
        lineNumber,
        commaIndex
      );

      if (!isLineStart && !isAfterComment) {
        if (!this.options.before && hasSpaceBefore) {
          this.addDefect(
            "There should be no space before ','.",
            lineNumber,
            actualColumn + 1,
            filePath,
            absolutePos
          );
        }
      }

      if (!isLineStart && !isAfterComment) {
        if (this.options.before && !hasSpaceBefore) {
          this.addDefect(
            "A space is required before ','.",
            lineNumber,
            actualColumn + 1,
            filePath,
            absolutePos
          );
        }
      }

      if (
        !isEndOfLine &&
        !this.options.after &&
        hasSpaceAfter &&
        !skipTrailingCommaAfterCheck &&
        !isLineEnd &&
        !skipAfterSpaceCheck
      ) {
        this.addDefect(
          "There should be no space after ','.",
          lineNumber,
          actualColumn + 1,
          filePath,
          absolutePos
        );
      }

      if (
        !isEndOfLine &&
        this.options.after &&
        !hasSpaceAfter &&
        !skipTrailingCommaAfterCheck &&
        !isLineEnd
      ) {
        this.addDefect(
          "A space is required after ','.",
          lineNumber,
          actualColumn + 1,
          filePath,
          absolutePos
        );
      }
    }
  }

  private calculateAbsolutePosition(
    lineNumber: number,
    columnIndex: number
  ): number {
    if (!this.arkFile) {
      return columnIndex;
    }

    const fileContent = this.arkFile.getCode();
    if (!fileContent) {
      return columnIndex;
    }

    const lines = fileContent.split("\n");
    let position = 0;

    for (let i = 0; i < lineNumber - 1; i++) {
      position += lines[i].length + 1;
    }

    position += columnIndex;
    return position;
  }

  private addDefect(
    message: string,
    line: number,
    column: number,
    filePath: string,
    startPos: number,
  ): Defects {
    const severity = this.rule.alert ?? this.metaData.severity;
    const defect = new Defects(
      line,
      column,
      column + 1,
      message,
      severity,
      this.rule.ruleId,
      filePath,
      this.metaData.ruleDocPath,
      true,
      false,
      false
    );

    let fix: RuleFix;
    if (message.includes("required after")) {
      fix = { range: [startPos + 1, startPos + 1], text: " " };
    } else if (message.includes("required before")) {
      fix = { range: [startPos, startPos], text: " " };
    } else if (message.includes("no space after")) {
      fix = { range: [startPos + 1, startPos + 2], text: "" };
    } else {
      fix = { range: [startPos - 1, startPos], text: "" };
    }

    const issue = new IssueReport(defect, fix);
    this.issues.push(issue);
    RuleListUtil.push(defect);
    
    return defect;
  }

  private cacheRanges(line: string): { stringRanges: Range[]; regExpRanges: Range[]; commentRanges: Range[] } {
    const stringRanges: Range[] = [];
    const regExpRanges: Range[] = [];
    const commentRanges: Range[] = [];
    
    let inSingleQuote = false;
    let inDoubleQuote = false;
    let inBacktick = false;
    let escaping = false;
    let stringStart = -1;
    
    const blockCommentRegex = /\/\*[\s\S]*?\*\//g;
    const lineCommentRegex = /\/\/.*/g;
    
    let blockMatch;
    while ((blockMatch = blockCommentRegex.exec(line)) !== null) {
      commentRanges.push({
        start: blockMatch.index + 2,
        end: blockMatch.index + blockMatch[0].length - 2
      });
    }
    
    let lineMatch;
    while ((lineMatch = lineCommentRegex.exec(line)) !== null) {
      commentRanges.push({
        start: lineMatch.index + 2,
        end: lineMatch.index + lineMatch[0].length
      });
    }
    
    for (let i = 0; i < line.length; i++) {
      const char = line[i];
      
      if (escaping) {
        escaping = false;
        continue;
      }
      
      if (char === '\\') {
        escaping = true;
        continue;
      }
      
      if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
        if (char === "'") {
          inSingleQuote = true;
          stringStart = i;
        } else if (char === '"') {
          inDoubleQuote = true;
          stringStart = i;
        } else if (char === '`') {
          inBacktick = true;
          stringStart = i;
        }
      } 
      else {
        if (char === "'" && inSingleQuote) {
          inSingleQuote = false;
          stringRanges.push({ start: stringStart, end: i });
        } else if (char === '"' && inDoubleQuote) {
          inDoubleQuote = false;
          stringRanges.push({ start: stringStart, end: i });
        } else if (char === '`' && inBacktick) {
          inBacktick = false;
          stringRanges.push({ start: stringStart, end: i });
        }
      }
    }
    
    const potentialRegExp = /\/(?![\/\*])(?:[^\/\\\n]|\\.)*\//g;
    let regMatch;
    while ((regMatch = potentialRegExp.exec(line)) !== null) {
      const start = regMatch.index;
      const end = start + regMatch[0].length - 1;
      
      const isInString = stringRanges.some(range => 
        (start >= range.start && start <= range.end) || 
        (end >= range.start && end <= range.end)
      );
      
      if (!isInString) {
        regExpRanges.push({ start, end });
      }
    }
    
    return { stringRanges, regExpRanges, commentRanges };
  }
  
  private isInRanges(position: number, ranges: Range[]): boolean {
    if (ranges.length === 0) {
      return false;
    }
    
    let left = 0;
    let right = ranges.length - 1;
    
    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const range = ranges[mid];
      
      if (position >= range.start && position <= range.end) {
        return true;
      }
      
      if (position < range.start) {
        right = mid - 1;
      } else {
        left = mid + 1;
      }
    }
    
    return false;
  }

  private isInString(line: string, position: number): boolean {
    let inSingleQuote = false;
    let inDoubleQuote = false;
    let inBacktick = false;
    let inTemplateExpr = false;
    let escaping = false;
    let braceCount = 0;

    for (let i = 0; i < position; i++) {
      const char = line[i];

      if (escaping) {
        escaping = false;
        continue;
      }

      if (char === "\\") {
        escaping = true;
        continue;
      }

      if (
        inBacktick &&
        !inTemplateExpr &&
        char === "$" &&
        i + 1 < line.length &&
        line[i + 1] === "{"
      ) {
        inTemplateExpr = true;
        braceCount = 1;
        i++;
        continue;
      }

      if (inTemplateExpr) {
        if (char === "{") {
          braceCount++;
        } else if (char === "}") {
          braceCount--;
          if (braceCount === 0) {
            inTemplateExpr = false;
          }
        }
        continue;
      }

      if (char === "'" && !inDoubleQuote && !inBacktick) {
        inSingleQuote = !inSingleQuote;
      } else if (char === '"' && !inSingleQuote && !inBacktick) {
        inDoubleQuote = !inDoubleQuote;
      } else if (char === "`" && !inSingleQuote && !inDoubleQuote) {
        inBacktick = !inBacktick;
      }
    }

    return inSingleQuote || inDoubleQuote || (inBacktick && !inTemplateExpr);
  }

  private isInRegExp(line: string, position: number): boolean {
    let lastSlashPos = -1;
    let inRegExp = false;
    let escaping = false;

    for (let i = 0; i < position; i++) {
      const char = line[i];

      if (escaping) {
        escaping = false;
        continue;
      }

      if (char === "\\") {
        escaping = true;
        continue;
      }

      if (char === "/" && !this.isInString(line, i)) {
        if (!inRegExp) {
          inRegExp = true;
          lastSlashPos = i;
        } else {
          inRegExp = false;
        }
      }
    }

    if (inRegExp && lastSlashPos !== -1) {
      for (let i = position + 1; i < line.length; i++) {
        const char = line[i];

        if (escaping) {
          escaping = false;
          continue;
        }

        if (char === "\\") {
          escaping = true;
          continue;
        }

        if (char === "/" && !this.isInString(line, i)) {
          return true;
        }
      }
    }

    return false;
  }

  private isGenericTrailingComma(line: string, position: number): boolean {
    let closingAngleFound = false;
    let i = position + 1;

    while (i < line.length && line[i] === " ") {
      i++;
    }

    if (i < line.length && line[i] === ">") {
      closingAngleFound = true;
    }

    if (!closingAngleFound) {
      return false;
    }

    let openAnglePos = -1;
    for (let j = position - 1; j >= 0; j--) {
      if (
        line[j] === "<" &&
        !this.isInString(line, j) &&
        !this.isInRegExp(line, j)
      ) {
        openAnglePos = j;
        break;
      }
    }

    if (openAnglePos === -1) {
      return false;
    }

    const beforeOpenAngle = line.substring(0, openAnglePos).trim();
    const isGenericContext =
      /\b(class|interface|type|function|const|let|var)\b/.test(
        beforeOpenAngle
      ) ||
      beforeOpenAngle.endsWith("=") ||
      /[a-zA-Z0-9_$]\s*$/.test(beforeOpenAngle);

    return isGenericContext;
  }

  private isCommaAfterComment(line: string, commaIndex: number): boolean {
    const beforeComma = line.substring(0, commaIndex).trim();

    if (beforeComma.endsWith("*/")) {
      const commentStartIndex = beforeComma.lastIndexOf("/*");
      if (commentStartIndex === -1) {
        return false;
      }

      const beforeComment = beforeComma.substring(0, commentStartIndex).trim();

      if (beforeComment.includes("[") || beforeComment.includes("{")) {
        let lastNonWhitespaceChar = "";
        for (let i = commentStartIndex - 1; i >= 0; i--) {
          if (line[i] !== " " && line[i] !== "\t") {
            lastNonWhitespaceChar = line[i];
            break;
          }
        }

        return (
          lastNonWhitespaceChar === "," ||
          lastNonWhitespaceChar === "[" ||
          lastNonWhitespaceChar === "{"
        );
      }
    }

    return false;
  }

  private isCommaBeforeComment(
    line: string,
    commaIndex: number,
    skipRanges: [number, number][]
  ): boolean {
    let i = commaIndex + 1;
    while (i < line.length && (line[i] === " " || line[i] === "\t")) {
      i++;
    }

    if (i < line.length) {
      if (
        line[i] === "/" &&
        i + 1 < line.length &&
        (line[i + 1] === "/" || line[i + 1] === "*")
      ) {
        return true;
      }
      return skipRanges.some(([start, _]) => start === i);
    }

    return false;
  }

  private isEndOfLine(line: string, commaIndex: number): boolean {
    const restOfLine = line.slice(commaIndex + 1).trim();
    
    if (restOfLine === '') {
      return true;
    }

    if (restOfLine.startsWith('//') || restOfLine.startsWith('/*')) {
      const lineContent = line.slice(commaIndex + 1);
      const commentStart = lineContent.indexOf('//') !== -1 
        ? lineContent.indexOf('//')
        : lineContent.indexOf('/*');
      
      const beforeComment = lineContent.substring(0, commentStart).trim();
      if (beforeComment !== '') {
        return false;
      }

      const afterCommentContent = lineContent.substring(commentStart);
      if (afterCommentContent.includes('\n') || 
          (afterCommentContent.startsWith('//') && line.trim() !== line.trimEnd())) {
        return false;
      }
    }

    const afterComma = line.slice(commaIndex + 1);
    const nextNonSpaceChar = afterComma.match(/\S/);
    
    if (nextNonSpaceChar) {
      const nextChar = nextNonSpaceChar[0];
      const nextCharIndex = afterComma.indexOf(nextChar);
      
      if (nextChar === '/' && afterComma[nextCharIndex + 1] === '/') {
        return false;
      }
      
      return (nextChar === ']' || nextChar === '}' || nextChar === ')') && nextCharIndex === 0;
    }

    return false;
  }
}